#include "../../syncup/fsnotify_imp.h"
#include <common.h>
#include <utils.h>

enum WatchKeyType {
    WATCH_KEY_REQUEST,
    WATCH_KEY_CANCEL_ALL,
    WATCH_KEY_NUM
};

#define EVENT_SIZE sizeof(FILE_NOTIFY_INFORMATION)
#define EVENT_GUESS_SIZE ((EVENT_SIZE + 16) & ~0x3) // DWORD boundary aligned
#define EVENT_BUF_SIZE (EVENT_GUESS_SIZE * 256)

struct watch_desc {
    HANDLE dir_hd;
    HANDLE file_hd;
    bool subtree;
    OVERLAPPED dir_ovlp;
    OVERLAPPED file_ovlp;
    char dir_evt_buf[EVENT_BUF_SIZE];
    char file_evt_buf[EVENT_BUF_SIZE];
};

/**
 * @param path  UTF-8 encoded path
 */
static struct watch_desc *
watch_desc_open(GString *path, bool subtree)
{
    HANDLE dir_hd, file_hd;
    struct watch_desc *wd = g_slice_new(struct watch_desc);
    gunichar2 *path2 = g_utf8_to_utf16(path->str, -1, NULL, NULL, NULL);
    if (path2 == NULL) {
        goto err;
    }
    dir_hd = CreateFileW(path2,
            FILE_LIST_DIRECTORY,
            FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE,
            NULL,
            OPEN_EXISTING,
            FILE_FLAG_OVERLAPPED | FILE_FLAG_BACKUP_SEMANTICS,
            NULL);
    file_hd = CreateFileW(path2,
            FILE_LIST_DIRECTORY,
            FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE,
            NULL,
            OPEN_EXISTING,
            FILE_FLAG_OVERLAPPED | FILE_FLAG_BACKUP_SEMANTICS,
            NULL);
    g_free(path2);
    if (dir_hd == INVALID_HANDLE_VALUE || file_hd == INVALID_HANDLE_VALUE) {
        goto err;
    }
    wd->dir_hd = dir_hd;
    wd->file_hd = file_hd;
    wd->subtree = subtree;
    memset(&wd->dir_ovlp, 0, sizeof (wd->dir_ovlp));
    memset(&wd->file_ovlp, 0, sizeof (wd->file_ovlp));
    return wd;

err:
    g_slice_free(struct watch_desc, wd);
    return NULL;
}

static void
watch_desc_close(struct watch_desc *wd)
{
    CloseHandle(wd->dir_hd);
    wd->dir_hd = NULL;
    CloseHandle(wd->file_hd);
    wd->file_hd = NULL;
    g_slice_free(struct watch_desc, wd);
}

int
fsnotify_desc_open(struct fsnotify_desc *desc)
{
    HANDLE hd = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
    if (hd == NULL) {
        return -1;
    }
    desc->hd = hd;
    desc->wd_map = g_hash_table_new(g_direct_hash, g_direct_equal);
    desc->buf = g_string_sized_new(4096);
    return 0;
}

void
fsnotify_desc_close(struct fsnotify_desc *desc)
{
    g_hash_table_foreach(desc->wd_map, (GHFunc) watch_desc_close, NULL);
    CloseHandle(desc->hd);
    desc->hd = NULL;
    g_hash_table_destroy(desc->wd_map);
    g_string_free(desc->buf, TRUE);
}



#define FSNOTIFY_DIR_WATCH_EVENT \
    FILE_NOTIFY_CHANGE_DIR_NAME
#define FSNOTIFY_FILE_WATCH_EVENT \
    (FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_LAST_WRITE)


static inline bool
fsnotify_add_watch(struct fsnotify_desc *desc, struct watch_desc *wd)
{
    HANDLE ret1, ret2;
    ret1 = CreateIoCompletionPort(wd->dir_hd, desc->hd, (ULONG_PTR) wd, 0);
    ret2 = CreateIoCompletionPort(wd->file_hd, desc->hd, (ULONG_PTR) wd, 0);
    return ret1 != NULL && ret2 != NULL;
}

static inline bool
watch_request_dir_notify(struct watch_desc *wd)
{
    return ReadDirectoryChangesW(wd->dir_hd,
            wd->dir_evt_buf, sizeof(wd->dir_evt_buf),
            wd->subtree,
            FSNOTIFY_DIR_WATCH_EVENT,
            NULL, &wd->dir_ovlp, NULL);
}

static inline bool
watch_request_file_notify(struct watch_desc *wd)
{
    return ReadDirectoryChangesW(wd->file_hd,
            wd->file_evt_buf, sizeof(wd->file_evt_buf),
            wd->subtree,
            FSNOTIFY_FILE_WATCH_EVENT,
            NULL, &wd->file_ovlp, NULL);
}

static inline bool
watch_request_notify(struct watch_desc *wd)
{
    if (!watch_request_dir_notify(wd)) {
        return false;
    }
    if (!watch_request_file_notify(wd)) {
        CancelIo(wd->dir_hd);
        return false;
    }
    return true;
}

static inline int
_watch_dentry(struct fsnotify_desc *desc, struct dentry *d, GString *path,
        bool subtree)
{
    g_assert(d != NULL);
    g_assert(path != NULL);

    watch_desc_t wd = watch_desc_open(path, subtree);
    if (!watch_desc_is_valid(wd)) {
        g_warning("can't open watch_desc");
        return -1;
    }
    if (fsnotify_add_watch(desc, wd) == FALSE) {
        goto err;
    }
    if (PostQueuedCompletionStatus(desc->hd, 0, WATCH_KEY_REQUEST,
            (LPOVERLAPPED) wd) == FALSE) {
        goto err;
    }
    d->wd = wd;
    g_hash_table_insert(desc->wd_map, d->wd, d);
    return 0;

err:
    watch_desc_close(wd);
    return -1;
}

int
fsnotify_watch_dentry(struct fsnotify_desc *desc, struct dentry *d,
        GString *path)
{
    return _watch_dentry(desc, d, path, false);
}

int
fsnotify_watch_dentry_recursively(struct fsnotify_desc *desc, struct dentry *d,
        GString *path)
{
    return _watch_dentry(desc, d, path, true);
}

static inline void
_rm_watch_dentry(struct fsnotify_desc *desc, struct dentry *d)
{
    watch_desc_close(d->wd);
    g_hash_table_remove(desc->wd_map, d->wd);
    d->wd = NULL;
}

void
fsnotify_rm_watch_dentry(struct fsnotify_desc *desc, struct dentry *d)
{
    _rm_watch_dentry(desc, d);
}

static void
fsnotify_rm_watch_dentry_recursively0(void *nouse, struct dentry *d,
        struct fsnotify_desc *desc)
{
    _rm_watch_dentry(desc, d);
    if (d->child) {
        g_tree_foreach(d->child,
                (GTraverseFunc) fsnotify_rm_watch_dentry_recursively0,
                desc);
    }
}

void
fsnotify_rm_watch_dentry_recursively(struct fsnotify_desc *desc,
        struct dentry *d)
{
    fsnotify_rm_watch_dentry_recursively0(NULL, d, desc);
}


#define NOTIFY_INFO_IS_END(info) ((info)->NextEntryOffset == 0)
#define NEXT_NOTIFY_INFO_POS(pos, len, info) \
    (NOTIFY_INFO_IS_END(info) ? (len) : ((pos) + (info)->NextEntryOffset))


/**
 * @internal
 * @brief Read next \c FILE_NOTIFY_INFORMATION
 * @param pos  Position of next info if any; otherwise that of the current one
 * @param info  Current info
 */
static inline FILE_NOTIFY_INFORMATION *
read_next_notify_info(struct fsnotify_desc *desc, struct watch_desc *wd,
        bool is_dir, ptrdiff_t pos, FILE_NOTIFY_INFORMATION *info)
{
    if (NOTIFY_INFO_IS_END(info)) {
        return NULL;
    }
    if(is_dir) {
        return (FILE_NOTIFY_INFORMATION *) &wd->dir_evt_buf[pos];
    } else {
        return (FILE_NOTIFY_INFORMATION *) &wd->file_evt_buf[pos];
    }
}

#define WD_TO_DENTRY(wd, desc) \
    ((struct dentry *) g_hash_table_lookup((desc)->wd_map, wd))

/**
 * @internal
 * @brief Convert \c FILE_NOTIFY_INFORMATION to \c fsnotify_event
 *
 * It also sets \p pos to the index of next \c FILE_NOTIFY_INFORMATION.
 */
static inline struct fsnotify_event *
convert_file_notify_info_to_fsnotify_evt(struct fsnotify_desc *desc,
        struct watch_desc *wd, bool is_dir, ptrdiff_t *pos, DWORD len,
        FILE_NOTIFY_INFORMATION *info)
{
    struct fsnotify_event *fs_evt;
    struct dentry *root, *d;
    gchar *path = NULL, *dirname = NULL;

    root = WD_TO_DENTRY(wd, desc);
    if (G_UNLIKELY(root == NULL)) {
        return NULL;
    }

    fs_evt = fsnotify_event_new(info->Action);
    if (is_dir) {
        fs_evt->mask |= FS_ISDIR;
    }
    if (info->FileNameLength > 0) {
        path = g_utf16_to_utf8(info->FileName, info->FileNameLength / 2,
                NULL, NULL, NULL);
        fsnotify_event_set_name(fs_evt, get_basename(path));
    }

    *pos = NEXT_NOTIFY_INFO_POS(*pos, len, info);


    /* NOTE: It's important to check any returned value of WD_TO_DENTRY,
     * because there are some race conditions that causes it to be NULL.
     * For example, a watched directory is moved out. Before we read events
     * into queue, some file is created in sub directory. Therefore, when we
     * read events, we get moved out event and file create event that points
     * to a dirent which will be removed due to moved out event.
     */
    dirname = g_path_get_dirname(path);
    if (strcmp(dirname, ".") == 0) {
        d = root;
    } else {
        d = dentry_lookup_subtree(root, dirname);
    }
    g_free(dirname);
    if (G_UNLIKELY(d == NULL)) {
        goto invalid_event_exit;
    }
    if (info->Action == FILE_ACTION_ADDED
            || info->Action == FILE_ACTION_REMOVED) {
        fs_evt->d.dir = d;
    } else if (info->Action == FILE_ACTION_MODIFIED) {
        fs_evt->d.dir = d;
        /* Ignore FILE_NOTIFY_CHANGE_LAST_WRITE change of directories */
        dentry_get_path(root, desc->buf);
        path_append_component(desc->buf, path);
        if (g_file_test(desc->buf->str, G_FILE_TEST_IS_DIR)) {
            goto invalid_event_exit;
        }
    } else if (info->Action == FILE_ACTION_RENAMED_OLD_NAME) {
        /* Note: In my test, FILE_ACTION_RENAMED_OLD_NAME's corresponding
         * FILE_ACTION_RENAMED_NEW_NAME must be next to it.
         * Moved out event is interpreted as delete and moved in as create.
         */
        FILE_NOTIFY_INFORMATION *next_info;

        fs_evt->d.rename.from = d;
        next_info = read_next_notify_info(desc, wd, is_dir, *pos, info);
        if (G_UNLIKELY(next_info == NULL)) {
            // This shouldn't happen
            fs_evt->d.rename.to = NULL;
        } else {
            if ((next_info->Action == FILE_ACTION_RENAMED_NEW_NAME)) {
                const char *basename;
                /* Consume next info */
                *pos = NEXT_NOTIFY_INFO_POS(*pos, len, next_info);

                fs_evt->mask = (FS_MOVE | (fs_evt->mask & FS_ISDIR));
                g_free(path);
                path = g_utf16_to_utf8(next_info->FileName,
                        next_info->FileNameLength / 2, NULL, NULL, NULL);
                basename = get_basename(path);
                if (strcmp(fs_evt->name, basename) != 0) {
                    fsnotify_event_set_new_name(fs_evt, basename);
                }
                dirname = g_path_get_dirname(path);
                if (strcmp(dirname, ".") == 0) {
                    d = root;
                } else {
                    d = dentry_lookup_subtree(root, dirname);
                }
                g_free(dirname);
                if (G_UNLIKELY(d == NULL)) {
                    goto invalid_event_exit;
                }
                fs_evt->d.rename.to = d;
            } else { /* irrelevant event */
                fs_evt->d.rename.to = NULL;
            }
        }
    } else if (info->Action == FILE_ACTION_RENAMED_NEW_NAME) {
        /* This shouldn't happen */
        fs_evt->d.rename.from = NULL;
        fs_evt->d.rename.to = d;
    } else {
        g_critical("unknown FILE_NOTIFY_INFORMATION (action: 0x%lx)",
                info->Action);
        goto invalid_event_exit;
    }

    g_free(path);
    return fs_evt;

invalid_event_exit:
    g_free(path);
    fsnotify_event_free(fs_evt);
    return NULL;
}

//static void
//wd_map_rm_watch_dentry(void *nouse, struct dentry *d,
//        struct fsnotify_desc *desc)
//{
//    _rm_watch_dentry(desc, d);
//}

static int
build_dentry_subtree0(struct dentry *d, GString *wpath)
{
    gsize end = wpath->len;
    HANDLE find_hd;
    WIN32_FIND_DATAW data;

    path_append_component_utf16(wpath, L"*");
    find_hd = FindFirstFileW((wchar_t *) wpath->str, &data);
    if (find_hd == INVALID_HANDLE_VALUE) {
        return -1;
    }
    g_string_truncate_utf16(wpath, end);

    do {
        if (data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY
                && wcscmp(data.cFileName, L".") != 0
                && wcscmp(data.cFileName, L"..") != 0) {
            struct dentry *d_child;
            gchar *name;

            name = g_utf16_to_utf8(data.cFileName, -1, NULL, NULL, NULL);
            d_child = dentry_new(name);
            g_free(name);
            dentry_add_child(d, d_child);
            path_append_component_utf16(wpath, data.cFileName);
            build_dentry_subtree0(d_child, wpath);
            g_string_truncate_utf16(wpath, end);
        }
    } while (FindNextFileW(find_hd, &data));
    if (GetLastError() != ERROR_NO_MORE_FILES) {
        goto err;
    }
    FindClose(find_hd);
    return 0;

err:
    FindClose(find_hd);
    return -1;
}

static int
build_dentry_subtree(struct dentry *d, GString *path)
{
    int ret;
    GString *wpath = g_string_new_utf16(path->str);
    ret = build_dentry_subtree0(d, wpath);
    g_string_free(wpath, TRUE);
    return ret;
}

int
fsnotify_read_event(struct fsnotify_desc *desc, struct list_head *evt_queue)
{
    DWORD len;
    ULONG_PTR key;
    OVERLAPPED *overlapped;

//    g_debug("before GetQueuedCompletionStatus()");
    if (GetQueuedCompletionStatus(desc->hd, &len, &key, &overlapped, INFINITE)
            == FALSE) {
        return -1;
    }
//    g_debug("after GetQueuedCompletionStatus()");
    do {
        if (G_UNLIKELY(key < WATCH_KEY_NUM)) {
            switch (key) {
            case WATCH_KEY_REQUEST:
//                g_debug("WATCH_KEY_REQUEST");
                if (!watch_request_notify((struct watch_desc *) overlapped)) {
                    g_critical("watch_request_notify() failed");
                }
                break;
            case WATCH_KEY_CANCEL_ALL:
//                g_debug("WATCH_KEY_CANCEL_ALL");
//                g_hash_table_foreach(desc->wd_map,
//                        (GHFunc) wd_map_rm_watch_dentry,
//                        desc);
                return 1;
            default:
                g_warning("unknown WatchKeyType: %lu", key);
                break;
            }
        } else {
            struct fsnotify_event *fs_evt;
            struct watch_desc *wd = (struct watch_desc *) key;
            bool is_dir = (overlapped == &wd->dir_ovlp);
//            g_debug("is_dir: %d", is_dir);

            ptrdiff_t pos = 0;
            while (pos < len) {
                FILE_NOTIFY_INFORMATION *info;
                if (is_dir) {
                    info = (FILE_NOTIFY_INFORMATION *) &wd->dir_evt_buf[pos];
                } else {
                    info = (FILE_NOTIFY_INFORMATION *) &wd->file_evt_buf[pos];
                }

                fs_evt = convert_file_notify_info_to_fsnotify_evt(desc, wd,
                        is_dir, &pos, len, info);
                if (fs_evt == NULL) {
                    continue;
                }

//                g_debug("fs_evt mask: 0x%x", fs_evt->mask);

                /*
                 * Process dentry tree if needed (FS_ISDIR is set)
                 */
                if (is_dir) {
                    struct dentry *d_child;

                    switch (fs_evt->mask & ~FS_ISDIR) {
                    case FS_CREATE:
                        d_child = dentry_new(fs_evt->name);
                        dentry_add_child(fs_evt->d.dir, d_child);
                        break;
                    case FS_DELETE:
                        d_child = dentry_lookup_child(fs_evt->d.dir,
                                fs_evt->name);
                        if (G_LIKELY(d_child)) {
                            fs_evt->garbage = dentry_steal_child(fs_evt->d.dir,
                                    fs_evt->name);
                        }
                        break;
                    case FS_MOVE:
                        d_child = dentry_lookup_child(fs_evt->d.rename.from,
                                fs_evt->name);
                        if (G_LIKELY(d_child)) {
                            if (fs_evt->new_name) {
                                dentry_rename(d_child, fs_evt->new_name);
                            }
                            dentry_move_child(fs_evt->d.rename.from,
                                    fs_evt->d.rename.to, d_child);
                        }
                        break;
                    case FS_MOVED_TO:
                        d_child = dentry_new(fs_evt->name);
                        dentry_add_child(fs_evt->d.rename.to, d_child);
                        dentry_get_path(d_child, desc->buf);
                        build_dentry_subtree(d_child, desc->buf);
                        break;
                    case FS_MOVED_FROM:
                        d_child = dentry_lookup_child(fs_evt->d.rename.from,
                                fs_evt->name);
                        if (G_LIKELY(d_child)) {
                            fs_evt->garbage = dentry_steal_child(
                                    fs_evt->d.rename.from, fs_evt->name);
                        }
                        break;
                    }
                }

                list_add_tail(&fs_evt->evt_list, evt_queue);
            }
            if (is_dir) {
                watch_request_dir_notify(wd);
            } else {
                watch_request_file_notify(wd);
            }
        }
    } while (GetQueuedCompletionStatus(desc->hd, &len, &key, &overlapped, 0));
    return 0;
}

void
fsnotify_cancel_read_event()
{
    PostQueuedCompletionStatus(fsnotify.desc.hd, 0, WATCH_KEY_CANCEL_ALL,
            (LPOVERLAPPED) 0);
}

/* For testing */
struct dentry *
fsnotify_wach_dir_recursively(struct fsnotify_desc *desc, const char *path)
{
    GString *path_buf = g_string_sized_new(4096);
    struct dentry *d;

    g_string_append(path_buf, path);
    d = dentry_new(path);
    fsnotify_watch_dentry_recursively(desc, d, path_buf);
    build_dentry_subtree(d, path_buf);

    g_string_free(path_buf, TRUE);
    return d;
}
